#include <cstring>
#include <iostream>
#include <mutex>
#include <unistd.h>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <backends/imgui_impl_glfw.h>
#include <backends/imgui_impl_opengl3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <imgui.h>

#include <renderer.h>
#include <texture.h>
#include <vertex_array.h>
#include <vertex_buffer.h>
#include <vertex_buffer_layout.h>

#include "game.h"

#if 0
#define IF_DEBUG(x, ...) x
#else
#define IF_DEBUG(x, ...) #__VA_ARGS__
#endif

Game g_game;
glm::mat4 view(1.0f);
std::mutex viewMutex;
double scaleTimes = 1;
int scaleXAcc, scaleYAcc;
int scaleX, scaleY;
int lastMouseX, lastMouseY;

void onKey(GLFWwindow *window, int32_t key, int32_t scancode, int32_t action, int32_t mods)
{
    g_game.OnKey(key, action, mods);
    if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
        glfwSetWindowShouldClose(window, GL_TRUE);
    }
}

void onMouseButton(GLFWwindow* window, int button, int action, int mods)
{
    g_game.OnMouseButton(button, action, mods);
}

void onCursorPos(GLFWwindow* window, double x, double y)
{
    if (x < 100) {
        scaleXAcc = 4;
    } else if (x > 1500) {
        scaleXAcc = -4;
    } else {
        scaleXAcc = 0;
    }

    if (y < 100) {
        scaleYAcc = -4;
    } else if (y > 800) {
        scaleYAcc = 4;
    } else {
        scaleYAcc = 0;
    }

    glm::vec4 v(x, 900 - y, 1, 1);
    {
        std::lock_guard<std::mutex> lock(viewMutex);
        v = view / v;
    }
    lastMouseX = v.x;
    lastMouseY = v.y;
    g_game.OnMousePosition(v.x, v.y);
}

void onScroll(GLFWwindow *window, double xoffset, double yoffset)
{
    if (yoffset < 0) {
        scaleTimes /= 1.1;
    }

    if (yoffset > 0) {
        scaleTimes *= 1.1;
    }

    scaleTimes = scaleTimes >= 4 ? 4 : scaleTimes;
    scaleTimes = scaleTimes <= 1.0 / 4 ? 1.0 / 4 : scaleTimes;
}

int main()
{
    if (!glfwInit()) {
        std::cerr << "glfwInit failed";
        return -1;
    }

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(1600, 900, "坦克大战", nullptr, nullptr);
    if (window == nullptr) {
        std::cerr << "glfwCreateWindow failed" << std::endl;
        std::cerr << "errno: " << errno << ", means " << strerror(errno) << std::endl;
        glfwTerminate();
        return -1;
    }

    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK) {
        std::cerr << "glewInit failed";
        return -1;
    }

    int32_t width, height;
    glfwGetFramebufferSize(window, &width, &height);
    glViewport(0, 0, width, height);
    glfwSetKeyCallback(window, onKey);
    glfwSetMouseButtonCallback(window, onMouseButton);
    glfwSetCursorPosCallback(window, onCursorPos);
    glfwSetScrollCallback(window, onScroll);

    std::cout << "GL Version: " << glGetString(GL_VERSION) << std::endl;
    {
        ImGui::CreateContext();
        ImGui_ImplGlfw_InitForOpenGL(window, true);
        ImGui_ImplOpenGL3_Init();
        ImGui::StyleColorsDark();

        // Settings
        GLCALL(glEnable(GL_BLEND));
        GLCALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); // transparent

        // Resource
        Renderer renderer;
        Shader shader("out/simple.shader");
        IF_DEBUG(Shader debugShader("out/debug.shader"));
        Shader bgShader("out/bg.shader");

        glm::mat4 projection = glm::ortho(0.0f, 1600.0f, 0.0f, 900.0f, -1.0f, 1.0f);

        g_game.ReadMap();

        uint32_t indices[] = {
            0, 1, 2,
            1, 2, 3,
        };
        IndexBuffer ib(indices, sizeof(indices) / sizeof(*indices));

        struct Model {
            std::string textureFilename;
            std::shared_ptr<VertexArray> va;
            std::shared_ptr<VertexBuffer> vb;
        };
        std::map<int, struct Model> models;
        std::map<std::string, std::unique_ptr<Texture>> textures;

        while (glfwWindowShouldClose(window) == 0) {
            renderer.Clear();

            ImGui_ImplOpenGL3_NewFrame();
            ImGui_ImplGlfw_NewFrame();
            ImGui::NewFrame();

            // {
            //     float vertexs[] = {
            //         -800, -450, 0.0f, 0.0f,                 // 0
            //          800, -450, 1.0f, 0.0f,                 // 1
            //          800,  450, 1.0f, 1.0f, // 2
            //         -800,  450, 0.0f, 1.0f, // 3
            //     };
            //     VertexArray va;
            //     VertexBuffer vb(vertexs, sizeof(vertexs));
            //     VertexBufferLayout layout;
            //     layout.Push<float>(2);
            //     layout.Push<float>(2);
            //     va.AddBuffer(vb, layout);
            //     shader.Bind();
            //     Texture texture("out/war_of_tank.png");
            //     texture.Bind(0);
            //     shader.SetUniform1i("u_Texture", 0);
            //     glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(800, 450, 0));
            //     shader.SetUniformMat4f("u_MVP", projection * model);
            //     renderer.Draw(va, ib, shader);
            // }

            decltype(view) usingView;
            {
                std::lock_guard<std::mutex> lock(viewMutex);
                view = glm::mat4(1.0f);
                view = glm::scale(view, glm::vec3(scaleTimes, scaleTimes, 1));
                view = glm::translate(view, glm::vec3(scaleX, scaleY, 0));
                usingView = view;

                scaleX += scaleXAcc;
                scaleY += scaleYAcc;
            }

            {
                glm::mat4 model(1.0f);
                model = glm::translate(model, glm::vec3(-1600, -900, 0));
                bgShader.SetUniformMat4f("u_MVP", projection * usingView * model);

                int32_t uielementID = -1;
                if (models.find(uielementID) == models.end()) {
                    struct TextureData data = {
                        .modelWidth = 4800,
                        .modelHeight = 2700,
                    };

                    float vertexs[8] = {};
                    for (int i = 0; i < 4; i++) {
                        vertexs[i * 2 + 0] = (i / 2) * data.modelWidth;
                        vertexs[i * 2 + 1] = (i % 2) * data.modelHeight;
                    }

                    models[uielementID].va = std::make_shared<VertexArray>();
                    models[uielementID].vb = std::make_shared<VertexBuffer>(vertexs, sizeof(vertexs));
                    VertexBufferLayout layout;
                    layout.Push<float>(2);
                    models[uielementID].va->AddBuffer(*models[uielementID].vb, layout);
                    models[uielementID].textureFilename = data.textureFilename;
                }

                glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
                renderer.Draw(*models[uielementID].va, ib, bgShader);
            }

            {
                g_game.VisitAllElement([&](const GameElementPtr e) {
                    glm::mat4 model(1.0f);
                    model = glm::translate(model, glm::vec3(e->x, e->y, 0));
                    model = glm::translate(model, glm::vec3(e->size / 2, e->size / 2, 0));
                    model = glm::rotate(model, static_cast<float>(-e->angle), glm::vec3(0, 0, 1));
                    model = glm::translate(model, glm::vec3(-e->size / 2, -e->size / 2, 0));
                    shader.SetUniformMat4f("u_MVP", projection * usingView * model);
                    IF_DEBUG(debugShader.SetUniformMat4f("u_MVP", projection * usingView * model));

                    int32_t uielementID = e->AsInt();
                    if (models.find(uielementID) == models.end()) {
                        auto data = e->GetTextureData();
                        if (textures.find(data.textureFilename) == textures.end()) {
                            textures[data.textureFilename] = std::make_unique<Texture>(data.textureFilename);
                        }

                        float vertexs[16] = {};
                        for (int i = 0; i < 4; i++) {
                            vertexs[i * 4 + 0] = (i / 2) * data.modelWidth + 0;
                            vertexs[i * 4 + 1] = (i % 2) * data.modelHeight + 0;
                            vertexs[i * 4 + 2] = (i / 2) * data.textureWidth + data.textureX;
                            vertexs[i * 4 + 3] = (i % 2) * data.textureHeight + data.textureY;
                        }

                        models[uielementID].va = std::make_shared<VertexArray>();
                        models[uielementID].vb = std::make_shared<VertexBuffer>(vertexs, sizeof(vertexs));
                        VertexBufferLayout layout;
                        layout.Push<float>(2);
                        layout.Push<float>(2);
                        models[uielementID].va->AddBuffer(*models[uielementID].vb, layout);
                        models[uielementID].textureFilename = data.textureFilename;
                    }

                    textures[models[uielementID].textureFilename]->Bind();
                    shader.SetUniform1i("u_Texture", 0);
                    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
                    renderer.Draw(*models[uielementID].va, ib, shader);
                    IF_DEBUG(glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);)
                    IF_DEBUG(renderer.Draw(*models[uielementID].va, ib, debugShader));
                }, IF_DEBUG(ImGui::Button("Next"), true));
            }

            ImGui::Text("Application average %.3f ms/frame (%.1f FPS)",
                1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
            ImGui::SliderInt("scaleX", &scaleX, -1000, 1000);
            ImGui::SliderInt("scaleY", &scaleY, -1000, 1000);
            ImGui::Render();
            ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

            glfwSwapBuffers(window);
            glfwPollEvents();
        }
    }

    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}
